Project 7 - Event Driven Finance

Helgi Ingimundarson - hi2179, Bradlee Spiece - bcs2149, Giovanni Gambarotta - gg2607, Jaydeep Soni - js4957
Python environment setup and functions can be found at the bottom of the file

Problem 1

Find two optionable stocks which were acquired in the period 1/1/2003 to 1/1/2013, one for cash and one for stock. Make note of the initial announcement dates.

The takeover deals we will study are -

(1) United Techologies' (UTX) acquisition of Goodrich Corporation (GR) for $18.4bn in cash - announced September 21, 2011

(2) AOL's (AOL) acquisition of Time Warner (TWX) for $156.14bn in stock - announced January 10, 2000

(3) JP Morgan's (JPM) acquisition of Bank One (ONE) for $58bn in stock - announced January 14, 2004

We now proceed to study the implied volatilities of the 40/50 delta front month and 30 delta back month options series around the temporal boundaries of the deal announcement dates.

Problem 2

Examine the option series for both stocks from 3 months before to one month after the announcement of the deal. We want to concentrate on the “50-delta” (ATM) and “40- delta” (just out-of-the-money) options in the front months and the “30-delta” (more out-of-the-money) options in the back months.
a) Do the option prices (implied volatilities) conform to the “caricature” discussed in class?
b) Were these deals leaked/anticipated? If so, how much lag time did the “good guessers” have?

In [12]:
# Query options trade data from 3 months prior to 1 month after the takeover announcement. 
# Goodrich (GR)
GR_id = 105338
GR_date_from = '2011-6-21'
GR_date_to = '2011-10-21'
GR_data_raw = query_opt_dataframe_f('PS7-options.sql', GR_id, GR_date_from, GR_date_to)
# Time Warner (TWX) 
TWX_id = 111051
TWX_date_from = '1999-10-10'
TWX_date_to = '2000-2-10'
TWX_data_raw = query_opt_dataframe_f('PS7-options.sql', TWX_id, TWX_date_from, TWX_date_to)
# Bank One (ONE) 
ONE_id = 101988
ONE_date_from = '2003-10-14'
ONE_date_to = '2004-2-14'
ONE_data_raw = query_opt_dataframe_f('PS7-options.sql', ONE_id, ONE_date_from, ONE_date_to)
In [13]:
# Interpolate 50,40 and 30 delta options for every maturity and strike
for delta in [.5,.4,.3]:
    GR_data_raw = GR_data_raw.groupby(['Date', 'Expiration','CallPut']).apply(
        lambda group: delta_interpolate(delta, group)
    )
    TWX_data_raw = TWX_data_raw.groupby(['Date', 'Expiration','CallPut']).apply(
        lambda group: delta_interpolate(delta, group)
    )
    ONE_data_raw = ONE_data_raw.groupby(['Date', 'Expiration','CallPut']).apply(
        lambda group: delta_interpolate(delta, group)
    )
In [14]:
# We look at the front month and back month options separately for each name
iv_cols = ['IVInterp_0.3','IVInterp_0.4','IVInterp_0.5']
# Find front and back expirations
GR_front_exp = GR_data_raw[(((np.isnan(GR_data_raw['IVInterp_0.5'])) &  (np.isnan(GR_data_raw['IVInterp_0.4']))) == False)].groupby('Date').min()['Expiration']
GR_back_exp = GR_data_raw[(((np.isnan(GR_data_raw['IVInterp_0.3']))) == False)].groupby('Date').max()['Expiration']
TWX_front_exp = TWX_data_raw[(((np.isnan(TWX_data_raw['IVInterp_0.5'])) &  (np.isnan(TWX_data_raw['IVInterp_0.4']))) == False)].groupby('Date').min()['Expiration']
TWX_back_exp = TWX_data_raw[(((np.isnan(TWX_data_raw['IVInterp_0.3']))) == False)].groupby('Date').max()['Expiration']
ONE_front_exp = ONE_data_raw[(((np.isnan(ONE_data_raw['IVInterp_0.5'])) &  (np.isnan(ONE_data_raw['IVInterp_0.4']))) == False)].groupby('Date').min()['Expiration']
ONE_back_exp = ONE_data_raw[(((np.isnan(ONE_data_raw['IVInterp_0.3']))) == False)].groupby('Date').max()['Expiration']
# Set up frames
GR_data = GR_data_raw.join(GR_front_exp, on=['Date'], rsuffix='_front')
GR_data = GR_data.join(GR_back_exp, on=['Date'], rsuffix='_back')
TWX_data = TWX_data_raw.join(TWX_front_exp, on=['Date'], rsuffix='_front')
TWX_data = TWX_data.join(TWX_back_exp, on=['Date'], rsuffix='_back')
ONE_data = ONE_data_raw.join(ONE_front_exp, on=['Date'], rsuffix='_front')
ONE_data = ONE_data.join(ONE_back_exp, on=['Date'], rsuffix='_back')
# Extract interpolated data - AVerage of call and put iv's for each date
GR_interp_front = GR_data[GR_data.Expiration == GR_data.Expiration_front].groupby(['Date'])[iv_cols].mean()
GR_interp_back = GR_data[GR_data.Expiration == GR_data.Expiration_back].groupby(['Date'])[iv_cols].mean()
TWX_interp_front = TWX_data[TWX_data.Expiration == TWX_data.Expiration_front].groupby(['Date'])[iv_cols].mean()
TWX_interp_back = TWX_data[TWX_data.Expiration == TWX_data.Expiration_back].groupby(['Date'])[iv_cols].mean()
ONE_interp_front = ONE_data[ONE_data.Expiration == ONE_data.Expiration_front].groupby(['Date'])[iv_cols].mean()
ONE_interp_back = ONE_data[ONE_data.Expiration == ONE_data.Expiration_back].groupby(['Date'])[iv_cols].mean()
In [15]:
# Plot GR
fig, ax1 = plt.subplots(figsize=(18,8))
GR_interp_front.fillna(method='ffill',inplace=True)
GR_interp_back.fillna(method='ffill',inplace=True)
ax2 = ax1.twinx()
ax1.plot(GR_interp_front.index, GR_interp_front['IVInterp_0.5'], 'b')
ax1.plot(GR_interp_front.index, GR_interp_front['IVInterp_0.4'], 'r')
ax1.plot(GR_interp_back.index, GR_interp_back['IVInterp_0.3'], 'g')
ax2.plot(GR_data_raw.Date, GR_data_raw.StockPrice, '--k')
plt.axvline(x='2011-9-16',color='r',ls='dashed')
plt.axvline(x='2011-9-22',color='b',ls='dashed') #Announcement after close
ax1.legend(['50 Delta Front','40 Delta Front','30 Delta Back'],loc=2),ax2.legend(loc=1)
plt.grid(True)
plt.title('Goodrich - Takeover announced: 2011-09-21')
plt.show()

Goodrich
a) Since this is a cash deal, we expect the implied volatilities for the options we are looking at to crash across the board following the announcement, i.e. to observe a regime shift of the implied volatility levels. This is indeed what happens, albeit in a phased manner owing to strong rumors of a takeover starting on the 15th of September 2011 (red dash). Once a cash deal for the takeover is confirmed on the 21st of September (blue dash), the LEAP IVs crash, as expected based on the caricature drawn out in class. This is accompanied by a two-phased price rise, first on the 15th when the rumors surfaced in the media and finally on the 21st when the deal is confirmed.

b) Rumours of the deal were leaked in the media beforehand on September 15, which the market seems to have acted upon - this explains the stock price and implied volatility behavior from September 15 until the deal was actually announced. There seems to have been an overall anticipation in the market for a deal based on these rumours, as opposed to someone trading on insider information. No prior ‘pleating’ of the IV surfaces is observed. The data indicates that some market participants acted on the rumors as soon as they came out (six days before the actual annoucement) and the sharp dip in implied volatilities along with the price increase on the 19th of September 2011 (two days before the announcement) seems to indicate that the market as a whole strongly believed the rumors at that point.

In [16]:
# Plot TWX
fig, ax1 = plt.subplots(figsize=(18,8))
TWX_interp_front.fillna(method='ffill',inplace=True)
TWX_interp_back.fillna(method='ffill',inplace=True)
ax2 = ax1.twinx()
ax1.plot(TWX_interp_front.index, TWX_interp_front['IVInterp_0.5'], 'b')
ax1.plot(TWX_interp_front.index, TWX_interp_front['IVInterp_0.4'], 'r')
ax1.plot(TWX_interp_back.index, TWX_interp_back['IVInterp_0.3'], 'g')
ax2.plot(TWX_data_raw.Date, TWX_data_raw.StockPrice, '--k')
plt.axvline(x='2000-01-10',color='b',ls='dashed')
ax1.legend(['50 Delta Front','40 Delta Front','30 Delta Back'],loc=2),ax2.legend(loc=1)
plt.grid(True)
plt.title('Time Warner - Takeover announced: 2000-1-10')
plt.show()

Time Warner
a) We expect to see an implied volatility regime change and since this is a stock deal, we expect the back month option series to peg to around the implied volatility level of the acquiring company, AOL, and this is indeed the case. In this particular scenario AOL has a higher IV level than TWX, and thus we observe a rise in the back month series IV. This conforms nicely with our expectations based on the caricature drawn out in class.

b) The AOL-Time Warner deal does not show any evidence of having been leaked/anticipated beforehand. No ‘pleating’ of the near month options series IVs is observed, and there are no sharp changes in the back month option series implied volatilities until the deal is confirmed.

In [17]:
# Plot ONE
fig, ax1 = plt.subplots(figsize=(18,8))
ONE_interp_front.fillna(method='ffill',inplace=True)
ONE_interp_back.fillna(method='ffill',inplace=True)
ax2 = ax1.twinx()
ax1.plot(ONE_interp_front.index, ONE_interp_front['IVInterp_0.5'], 'b')
ax1.plot(ONE_interp_front.index, ONE_interp_front['IVInterp_0.4'], 'r')
ax1.plot(ONE_interp_back.index, ONE_interp_back['IVInterp_0.3'], 'g')
ax2.plot(ONE_data_raw.Date, ONE_data_raw.StockPrice, '--k')
plt.axvline(x='2004-1-15',color='b',ls='dashed')
ax1.legend(['50 Delta Front','40 Delta Front','30 Delta Back'],loc=2),ax2.legend(loc=1)
plt.grid(True)
plt.title('Bank One - Takeover announced: 2004-01-14')
plt.show()

Bank One
a) The analysis for the Time Warner deal pretty much applies to this case as well. JP Morgan's long term implied volatility level was slightly higher than Bank One's and we can observe that the back month IV series rises to JP Morgans's level on the announcement date. This shift conforms nicely with our expectations based on the caricature drawn out in class.

b) The JP Morgan-Bank One deal does not show any evidence of having been leaked/anticipated beforehand. No ‘pleating’ of the near month options series IVs is observed, and there are no sharp changes in the back month option series implied volatilities until the deal is confirmed.

Problem 3

For the stock CEPH, look at the period of 01/2011 to 05/2011.
a) What significant event seems to happen? When?
b) The term structure of volatility refers to the variation of implied volatility with respect to maturity date. Plot the “30/40/50-delta” implied volatilities with respect to expiration from 01/2011 through 05/2011. What, if anything, do you see?

In [18]:
# Candlestick plot of Open - High - Low- Close chart
ceph_stock = query_dataframe_f('Q3-stock.sql')
ceph_stock['date2num'] = ceph_stock['Date'].apply(date2num)
plot_candlestick(ceph_stock[['date2num', 'OpenPrice', 'AskHigh', 'BidLow', 'ClosePrice']].values)
plt.title('Cephalon Stock Price'),plt.ylabel('Price');

There are two events that stick out to us. First, on March 29th, Valeant launches a hostile takeover bid on Cephalon, for $73/share. The bid was publically rejected by Cephalon on April 5th but that did not end Valeant's interest which launched a proxy war to take over Cephalon's board. The stocks persistance at and above the bid level seemed to indicate the market's anticipation of further negotiations or a future takeover at an even higher price.

Second, on May 2, Valent's takeover attempt officially ends when Teva Pharmaceuticals announced that they would acquire Cephalon for $81.50 per share and the stock trades around this point for the rest of its listed time.

We will now plot the implied volatilities for the 40- and 50-delta current-expiration options and the 30-delta LEAPs. To get the exact delta, we will interpolate between the prices surrounding a delta value to approximate an exact delta, and do our implied volatility calculations given that data. See Appendix for full term structures.

In [20]:
# Query Cephalon options trade data from 2011-1-1 tot 2011-6-1
CEPH_id = 102886
CEPH_date_from = '2011-1-1'
CEPH_date_to = '2011-6-1'
CEPH_data_raw= query_opt_dataframe_f('PS7-options.sql', CEPH_id, CEPH_date_from, CEPH_date_to)
In [21]:
# Interpolate 50,40 and 30 delta options for every maturity and strike
for delta in [.5,.4,.3]:
    CEPH_data_raw = CEPH_data_raw.groupby(['Date', 'Expiration','CallPut']).apply(
        lambda group: delta_interpolate(delta, group)
    )
In [22]:
# We look at the front month and back month options separately
iv_cols = ['IVInterp_0.3','IVInterp_0.4','IVInterp_0.5']
# Find front and back expirations
CEPH_front_exp = CEPH_data_raw[(((np.isnan(CEPH_data_raw['IVInterp_0.5'])) &  (np.isnan(CEPH_data_raw['IVInterp_0.4']))) == False)].groupby('Date').min()['Expiration']
CEPH_back_exp = CEPH_data_raw[(((np.isnan(CEPH_data_raw['IVInterp_0.3']))) == False)].groupby('Date').max()['Expiration']
# Set up frames
CEPH_data = CEPH_data_raw.join(CEPH_front_exp, on=['Date'], rsuffix='_front')
CEPH_data = CEPH_data.join(CEPH_back_exp, on=['Date'], rsuffix='_back')
# Extract interpolated data
CEPH_interp_front = CEPH_data[CEPH_data.Expiration == CEPH_data.Expiration_front].groupby(['Date'])[iv_cols].mean()
CEPH_interp_back = CEPH_data[CEPH_data.Expiration == CEPH_data.Expiration_back].groupby(['Date'])[iv_cols].mean()
In [23]:
# Plot CEPH
fig, ax1 = plt.subplots(figsize=(18,8))
CEPH_interp_front.fillna(method='bfill',inplace=True)
CEPH_interp_back.fillna(method='ffill',inplace=True)
ax2 = ax1.twinx()
ax1.plot(CEPH_interp_front.index, CEPH_interp_front['IVInterp_0.5'], 'b')
ax1.plot(CEPH_interp_front.index, CEPH_interp_front['IVInterp_0.4'], 'r')
ax1.plot(CEPH_interp_back.index, CEPH_interp_back['IVInterp_0.3'], 'g')
ax2.plot(CEPH_data_raw.Date, CEPH_data_raw.StockPrice, '--k')
plt.axvline(x='2011-03-18',color='y',ls='dashed')
plt.axvline(x='2011-03-25',color='y',ls='dashed')
plt.axvline(x='2011-03-30',color='r',ls='dashed')
plt.axvline(x='2011-04-06',color='g',ls='dashed')
plt.axvline(x='2011-05-02',color='b',ls='dashed')
ax1.legend(['50 Delta Front','40 Delta Front','30 Delta Back'],loc=2),ax2.legend(loc=1)
plt.grid(True)
plt.title('Cephalon - Takeover Bid (Valeant): 2011-03-29 - Takeover Announced (Teva): 2011-05-02')
plt.show()

Observations:

(1) The implied volatility of the 30-delta LEAP crashes after the takeover bid is launched by Valeant (red dash) and stays on that level even after the bid is publically rejected (green dash).

(2) Private bids were launched by Valeant on March 18 and 25 (yellow dashes) for Cephalon, however there is nothing in the IV plots before these dates that indicates that information about the bids or takeover attempt was leaked before the public announcement March 29. The volatility spike before March 18 is due to a switch of front months.

(3) Surely enough, when Teva Pharmaceuticals announces it will takeover Cephalon in late April (blue dash), we observe a clear IV level regime change again. The average IV level falls across the board for both front options, and the LEAP IVs fall even further than before, as we would expect, in line with the caricature drawn out in class of how IVs tend to behave around takeover events.

This in interesting case since it involved two competing parties and we can observe clear shifts in implied volatility levels when each party enters the stage as the leading takeover candidate. Nothing in the data indicates that either the initial takeover attempt by Valeant or the eventual aquisition by Teva was prematurely anticipated by market participants.

Appendix

For confirmation of our previous observations we analyse the full term structures of 30, 40 and 50 delta options for all of the cases above.
Missing data points occur when options with desired characteristics are not obtainable.

In [28]:
# GR - IV term structures
iv_cols=['IVInterp_0.5','IVInterp_0.4','IVInterp_0.3']
maxy = max(np.max(GR_data[iv_cols]))
for dt,row in GR_data[(GR_data.Date > '2011-9-10') & (GR_data.Date < '2011-9-28')].groupby(['Date']):
    a = 1
    fig, ax = plt.subplots(figsize=(18,8))
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[0]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[1]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[2]])
    ax.legend(['50 Delta','40 Delta','30 Delta'],loc=1)
    plt.grid(True)
    plt.title('Goodrich - IV Term Structure - {}'.format(pd.to_datetime(str(dt)).strftime('%Y/%m/%d')))
    ax.set_ylabel('Implied Volatility'), ax.set_ylim([0,maxy])
    ax.set_xlabel('Time To Maturity'), ax.set_xlabel('Time to maturity - Y')
    plt.show() 

Goodrich Evidence that the deal was anticipated as early as September 15 (pleating of the term structures). Clear regime change after announcement on September 21.

In [30]:
# TWX - IV term structures
maxy = max(np.max(TWX_data[iv_cols]))
for dt,row in TWX_data[(TWX_data.Date > '2000-1-1') & (TWX_data.Date < '2000-1-20')].groupby(['Date']):
    a = 1
    fig, ax = plt.subplots(figsize=(18,8))
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[0]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[1]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[2]])
    ax.legend(['50 Delta','40 Delta','30 Delta'],loc=1)
    plt.grid(True)
    plt.title('Time Warner - IV Term Structure - {}'.format(pd.to_datetime(str(dt)).strftime('%Y/%m/%d')))
    ax.set_ylabel('Implied Volatility'), ax.set_ylim([0,maxy])
    ax.set_xlabel('Time To Maturity'), ax.set_xlabel('Time to maturity - Y')
    plt.show() 

Time Warner Nothing that indicates the deal on January 10 was anticpated, clear IV level shift after deal.

In [29]:
# ONE - IV term structures
maxy = max(np.max(ONE_data[iv_cols]))
for dt,row in ONE_data[(ONE_data.Date > '2004-1-4') & (ONE_data.Date < '2004-1-24')].groupby(['Date']):
    a = 1
    fig, ax = plt.subplots(figsize=(18,8))
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[0]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[1]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[2]])
    ax.legend(['50 Delta','40 Delta','30 Delta'],loc=1)
    plt.grid(True)
    plt.title('Bank One - IV Term Structure - {}'.format(pd.to_datetime(str(dt)).strftime('%Y/%m/%d')))
    ax.set_ylabel('Implied Volatility'), ax.set_ylim([0,maxy])
    ax.set_xlabel('Time To Maturity'), ax.set_xlabel('Time to maturity - Y')
    plt.show() 

Bank One Nothing that indicates the deal on January 14 was anticpated, subtle IV regime change after deal.

In [31]:
# CEPH - IV term structures
maxy = max(np.max(CEPH_data[iv_cols]))
for dt,row in CEPH_data[(CEPH_data.Date > '2011-1-1') & (CEPH_data.Date < '2011-6-1')].groupby(['Date']):
    a = 1
    fig, ax = plt.subplots(figsize=(18,8))
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[0]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[1]])
    ax.plot(row.groupby('ExpirationY')[iv_cols].mean().index, row.groupby('Expiration')[iv_cols].mean()[iv_cols[2]])
    ax.legend(['50 Delta','40 Delta','30 Delta'],loc=1)
    plt.grid(True)
    plt.title('Cephalon - IV Term Structure - {}'.format(pd.to_datetime(str(dt)).strftime('%Y/%m/%d')))
    ax.set_ylabel('Implied Volatility'), ax.set_ylim([0,maxy])
    ax.set_xlabel('Time To Maturity'), ax.set_xlabel('Time to maturity - Y')
    plt.show() 

Cephalon Clear regime changes when (1) Valeant launches bid on March 29 and (2) Teva takeover is made public on May 2. Nothing indicating private Valeant bids were leaked or anticipated.

Functions

In [8]:
import sys
sys.path.append('../utils/')
import datetime
import pandas as pd
import numpy as np
from pygments import highlight
from pygments.lexers.sql import SqlLexer
from pygments.formatters import HtmlFormatter, LatexFormatter
from IPython import display
import functools as ft
import matplotlib.pyplot as plt
from sqlalchemy import create_engine
import scipy as sp
import scipy.interpolate
from scipy.interpolate import interp1d
from __future__ import division
import numpy as np
import matplotlib.dates as mdates
from scipy.optimize import minimize
from matplotlib.finance import candlestick_ohlc
from matplotlib.dates import date2num
import datetime
from datetime import date
import math
%matplotlib inline


CONNECTION_STRING = 'mssql+pymssql://IVYuser:resuyvi@vita.ieor.columbia.edu'

# Gets the database connection
def get_connection():
    engine = create_engine(CONNECTION_STRING)
    return engine.connect()

# Query database and return results in dataframe
def query_dataframe(query, connection=None):
    if connection is None:
        connection = get_connection()
    return pd.read_sql(query, connection)

# Query database using external file and return results in dataframe
def query_dataframe_f(filename, connection=None):
    if connection is None:
        connection = get_connection()
    with open(filename, 'r') as handle:
        return pd.read_sql(handle.read(), connection)

# Print sql query and query results
def print_and_query(filename, connection=None, use_latex=False):
    if connection is None:
        connection = get_connection()
    with open(filename, 'r') as handle:
        sql = handle.read()
        if use_latex:
            display_obj = display.Latex(highlight(
                sql, SqlLexer(), LatexFormatter()))
        else:
            formatter = HtmlFormatter()
            display_obj = display.HTML(
                '<style type="text/css">{}</style>{}'
                .format(
                formatter.get_style_defs('.highlight'),
                highlight(sql, SqlLexer(), formatter)))
        display.display(
            display_obj,
            pd.read_sql(sql, connection)
        ) 

def query_dataframe_f(filename, connection=None):
    if connection is None:
        connection = get_connection()
    with open(filename, 'r') as handle:
        return pd.read_sql(handle.read(), connection)

def query_opt_dataframe_f(file, sec_id, Date_from, Date_to):
    sql_raw = open(file, 'r').read()
    sql_format = sql_raw.format(
        security_id = sec_id, 
        date_from = Date_from, # yyyy-MM-dd
        date_to = Date_to # yyyy-MM-dd
    )
    data = query_dataframe(sql_format) 
    return data
In [9]:
'''
    American Option Pricer with dividends - Function call
    ABM(ft.partial(VP,K=Strike,CallPut='OptType'),StockPrice,TimeToMaturity(1/360), 
               Interest Rate, Volatility, TreeSteps, Dividends))
    Dividends are given in the format np.array([[time, dividend],....,])
'''

def BPTree(n, S, u, d, ex_div):
    # Creating a binomial tree with dividends adjustment
    r = [np.array([S])]
    for i in range(n):
        if (i in ex_div[:,0]):
            adj = ex_div[ex_div[:,0]==i][0,1]
            r.append(np.concatenate((r[-1][:1]*u-adj, r[-1]*d-adj)))
        else: r.append(np.concatenate((r[-1][:1]*u, r[-1]*d))) 
    return r

def GBM(R, P, S, T, r, v, n, ex_div):
    # Function returns the American option price
    t = float(T)/n
    T_range = np.arange(0,T+t,t)
    # Selecting dividends before option's maturity date
    curr_div = ex_div[ex_div[:,0]<=T]
    div_idx = []
    # Indexing the dividends ex date in the binomial tree
    for i in range (len(curr_div[:,0])):
        T_diff = T_range[1:]-curr_div[i,0]
        T_diff[T_diff<0]=100000
        div_idx.append(np.argmin(T_diff))
    curr_div[:,0] = np.array(div_idx)
    # Defining up/down binomial step coefficients 
    u = np.exp(v * np.sqrt(t))
    d = 1./u
    p = (np.exp(r * t) - d)/(u - d)
    # Creating the binomial tree
    ptree = BPTree(n, S, u, d, curr_div)[::-1]
    # Defining a function for discounting and P-measure in the tree
    R_ = ft.partial(R, np.exp(-r*t), p)
    # Discounting through the tree with american exercise option
    return ft.reduce(R_, map(P, ptree))[0]

def American(D, p, a, b): 
    # Selecting maximum between continuation and intrinsic option value
    return np.maximum(b, D*(a[:-1]*p + a[1:]*(1-p)))

def VP(S, K, CallPut): 
    # Intrinsic value
    if (CallPut=='C'): return np.maximum(S-K, 0)
    else: return np.maximum(K-S, 0)
    
ABM = ft.partial(GBM, American)
In [10]:
def iv_solver(mbbo_price, stock_price, strike, expiration, rate, is_call, iv0, steps):
    if is_call:
        cp = 'C'
    else:
        cp = 'P'
    cost = lambda vol: ((mbbo_price - 
                           ABM(ft.partial(VP,K=strike,CallPut=cp),
                               stock_price,
                               expiration,
                               rate, 
                               vol, 
                               steps, 
                               np.array([0,0,0,0]).reshape(2,2)))**2)
    cons = ({'type': 'ineq',
         'fun' : lambda x: np.array(x),
         'jac': lambda x: np.array([1.0])
        })
    res = minimize(cost, iv0, constraints=cons)
    return res.x

def delta_interpolate(delta, group):
    if (len(group) < 2):
        return group
    # Given a row of data and a delta value we want to find
    # (i.e. 50 delta) get the interpolated strike/price of an option,
    # then use that to back out the implied volatility.
    
    delta_col = 'IVInterp_{}'.format(delta)
    try:
        is_call = group['CallPut'].values[0] == 'C'
    except KeyError:
        print(group['CallPut'].values)
        group[delta_col] = np.array([np.nan]*len(group))
        return group
    if is_call:
        _delta = delta
    else:
        _delta = -delta
    
    f = interp1d(group['Delta'], group[['Strike', 'MBBO', 'ImpliedVolatility']], axis=0)
    
    try:
        delta_strike, delta_price, delta_iv = f(_delta)
    except ValueError:
        group[delta_col] = np.array([np.nan]*len(group))
        return group
    
    steps = 100
    interp_sigma = iv_solver(
        delta_price,
        group['StockPrice'].values[0],
        delta_strike,
        group['ExpirationY'].values[0],
        group['ZeroRate'].values[0],
        is_call,
        delta_iv,
        steps
    )
    group[delta_col] = np.array([interp_sigma]*len(group))
    return group    
In [11]:
# Much of the code for candlestick plotting comes from this page:
# http://matplotlib.org/examples/pylab_examples/finance_demo.html
from matplotlib.dates import DateFormatter, WeekdayLocator,\
    DayLocator, MONDAY
def plot_candlestick(values):
    mondays = WeekdayLocator(MONDAY)        # major ticks on the mondays
    alldays = DayLocator()              # minor ticks on the days
    weekFormatter = DateFormatter('%b %d')  # e.g., Jan 12
    dayFormatter = DateFormatter('%d')      # e.g., 12
    
    fig, ax = plt.subplots()
    fig.subplots_adjust(bottom=0.2)
    ax.xaxis.set_major_locator(mondays)
    ax.xaxis.set_minor_locator(alldays)
    ax.xaxis.set_major_formatter(weekFormatter)
    
    candlestick_ohlc(ax, values, width=1, colorup='g', colordown='r', alpha=0.75)
    ax.xaxis_date()
    ax.autoscale_view()
    plt.setp(plt.gca().get_xticklabels(), rotation=45, horizontalalignment='right')
    
    plt.grid(True)
    plt.gcf().set_size_inches(20, 10)
    
    return fig, ax